/**
 * @extends Ext.ux.event.Driver Event recorder.
 */
Ext.define('Ext.ux.event.Recorder', function() {
	function samePt(pt1, pt2) {
		return pt1.x == pt2.x && pt1.y == pt2.y;
	}

	return {
		extend : 'Ext.ux.event.Driver',

		eventsToRecord : function() {
			var key = {
				kind : 'keyboard',
				modKeys : true,
				key : true,
				bubbles : true
			}, mouse = {
				kind : 'mouse',
				button : true,
				modKeys : true,
				xy : true,
				bubbles : true
			};

			return {
				keydown : key,
				keypress : key,
				keyup : key,

				mousemove : mouse,
				mouseover : mouse,
				mouseout : mouse,
				click : mouse,
				// mousewheel: Ext.apply({ wheel: true }, mouse),
				mousedown : mouse,
				mouseup : mouse,

				scroll : {
					kind : 'misc',
					bubbles : false
				}
			};
		}(),

		ignoreIdRegEx : /ext-gen(?:\d+)/,

		constructor : function(config) {
			var me = this, events = config && config.eventsToRecord;

			if (events) {
				me.eventsToRecord = Ext.apply(Ext.apply({}, me.eventsToRecord), // duplicate
						events); // and merge
				delete config.eventsToRecord; // don't smash
			}

			me.callParent(arguments);

			me.addEvents(
					/**
					 * @event add Fires when an event is added to the recording.
					 * @param {Ext.ux.event.Recorder}
					 *            this
					 * @param {Object}
					 *            eventDescriptor The event descriptor.
					 */
					'add',

					/**
					 * @event coalesce Fires when an event is coalesced. This
					 *        edits the tail of the recorded event list.
					 * @param {Ext.ux.event.Recorder}
					 *            this
					 * @param {Object}
					 *            eventDescriptor The event descriptor that was
					 *            coalesced.
					 */
					'coalesce');

			me.clear();
			me.modKeys = [];
			me.attachTo = me.attachTo || window;
		},

		clear : function() {
			this.eventsRecorded = [];
		},

		coalesce : function(rec) {
			var me = this, events = me.eventsRecorded, length = events.length, tail = length
					&& events[length - 1], tailPrev;

			if (!tail) {
				return false;
			}

			if (rec.type == 'mousemove') {
				if (tail.type == 'mousemove' && rec.ts - tail.ts < 200) {
					rec.ts = tail.ts;
					events[length - 1] = rec;
					return true;
				}
			} else if (rec.type == 'click') {
				if (length >= 2 && tail.type == 'mouseup'
						&& (tailPrev = events[length - 2]).type == 'mousedown') {
					if (rec.button == tail.button
							&& rec.button == tailPrev.button
							&& rec.target == tail.target
							&& rec.target == tailPrev.target
							&& samePt(rec, tail) && samePt(rec, tailPrev)) {
						events.pop(); // remove mouseup
						tailPrev.type = 'mduclick';
						return true;
					}
				}
			}

			return false;
		},

		getElementXPath : function(el) {
			var me = this, good = false, xpath = [], count, sibling, t, tag;

			for (t = el; t; t = t.parentNode) {
				if (t == me.attachTo.document.body) {
					xpath.unshift('~');
					good = true;
					break;
				}
				if (t.id && !me.ignoreIdRegEx.test(t.id)) {
					xpath.unshift('#' + t.id);
					good = true;
					break;
				}

				for (count = 1, sibling = t; !!(sibling = sibling.previousSibling);) {
					if (sibling.tagName == t.tagName) {
						++count;
					}
				}

				tag = t.tagName.toLowerCase();
				if (count < 2) {
					xpath.unshift(tag);
				} else {
					xpath.unshift(tag + '[' + count + ']');
				}
			}

			return good ? xpath.join('/') : null;
		},

		getRecordedEvents : function() {
			return this.eventsRecorded;
		},

		// DOMNodeInserted
		onDomInsert : function(event, target) {
			this.watchTree(target);
		},

		// DOMNodeRemoved
		onDomRemove : function(event, target) {
			this.unwatchTree(target);
		},

		onEvent : function(e) {
			var me = this, info = me.eventsToRecord[e.type], root, modKeys, elXY, rec = {
				type : e.type,
				ts : me.getTimestamp(),
				target : me.getElementXPath(e.target)
			}, xy;

			if (!rec.target) {
				return;
			}
			root = e.target.ownerDocument;
			root = root.defaultView || root.parentWindow; // Standards || IE
			if (root !== me.attachTo) {
				return;
			}

			if (info.xy) {
				xy = e.getXY();
				if (rec.target) {
					elXY = Ext.fly(e.getTarget()).getXY();
					xy[0] -= elXY[0];
					xy[1] -= elXY[1];
				}
				rec.x = xy[0];
				rec.y = xy[1];
			}

			if (info.button) {
				rec.button = e.button;
			}

			if (info.wheel) {
				rec.wheel = e.getWheelDelta();
			}

			if (info.modKeys) {
				me.modKeys[0] = e.altKey ? 'A' : '';
				me.modKeys[1] = e.ctrlKey ? 'C' : '';
				me.modKeys[2] = e.metaKey ? 'M' : '';
				me.modKeys[3] = e.shiftKey ? 'S' : '';

				modKeys = me.modKeys.join('');
				if (modKeys) {
					rec.modKeys = modKeys;
				}
			}

			if (info.key) {
				rec.charCode = e.getCharCode();
				rec.keyCode = e.getKey();
			}

			if (me.coalesce(rec)) {
				me.fireEvent('coalesce', me, rec);
			} else {
				me.eventsRecorded.push(rec);
				me.fireEvent('add', me, rec);
			}
		},

		onStart : function() {
			var me = this, on = {
				DOMNodeInserted : me.onDomInsert,
				DOMNodeRemoved : me.onDomRemove,
				scope : me
			}, nonBubbleEvents = (me.nonBubbleEvents = {}), ddm = me.attachTo.Ext.dd.DragDropManager, evproto = me.attachTo.Ext.EventObjectImpl.prototype;

			me.watchingNodes = {};

			Ext.Object.each(me.eventsToRecord, function(name, value) {
						if (value) {
							if (value.bubbles) {
								on[name] = me.onEvent;
							} else {
								nonBubbleEvents[name] = value;
							}
						}
					});

			me.ddmStopEvent = ddm.stopEvent;
			ddm.stopEvent = Ext.Function.createSequence(ddm.stopEvent,
					function(e) {
						me.onEvent(e);
					});
			me.evStopEvent = evproto.stopEvent;
			evproto.stopEvent = Ext.Function.createSequence(evproto.stopEvent,
					function() {
						me.onEvent(this);
					});

			var body = me.attachTo.Ext.getBody();
			body.on(on);
			me.watchTree(body.dom);
		},

		onStop : function() {
			var me = this, body = me.attachTo.Ext.getBody();

			Ext.Object.each(me.eventsToRecord, function(name, value) {
						if (value) {
							body.un(name, me.onEvent, me);
						}
					});

			me.attachTo.Ext.dd.DragDropManager.stopEvent = me.ddmStopEvent;
			me.attachTo.Ext.EventObjectImpl.prototype.stopEvent = me.evStopEvent;

			me.unwatchTree(body.dom);
		},

		watchTree : function(root) {
			if (root.nodeType != 1) {
				return; // only ELEMENT_NODE's please...
			}

			var me = this, id = (root.tagName == 'BODY') ? '$' : root.id, watchingNodes = me.watchingNodes;

			if (id && !watchingNodes[id]) {
				var on = {
					scope : me
				};

				Ext.Object.each(me.nonBubbleEvents, function(name, value) {
							if (value) {
								on[name] = me.onEvent;
							}
						});
				me.attachTo.Ext.fly(root).on(on);
				watchingNodes[id] = true;
				// console.log('watch '+root.tagName+'#'+id);
			}

			Ext.each(root.childNodes, me.watchTree, me);
		},

		unwatchTree : function(root) {
			if (root.nodeType != 1) {
				return; // only ELEMENT_NODE's please...
			}

			var me = this, id = (root.tagName == 'BODY') ? '$' : root.id, watchingNodes = me.watchingNodes;

			if (id && !watchingNodes[id]) {
				Ext.Object.each(me.nonBubbleEvents, function(name, value) {
							me.attachTo.Ext.fly(root).un(name, me.onEvent, me);
						});
				delete watchingNodes[id];
				// console.log('unwatch '+root.tagName+'#'+id);
			}

			Ext.each(root.childNodes, me.unwatchTree, me);
		}
	};
}());
